package com.kevin.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import static com.kevin.util.DateTimeUtil.DateTimePattern.NONE_SEPARATOR;
import static com.kevin.util.DateTimeUtil.dateTime2String;
import static com.kevin.util.DateTimeUtil.transform;

/**
 * kevin<br/>
 * 2022/8/8 15:55<br/>
 */
public class ExcelUtil<T> {

    /**
     * 工作薄
     */
    private SXSSFWorkbook wb;

    /**
     * 工作表
     */
    private SXSSFSheet sheet;

    /**
     * 单元格样式
     */
    private Map<String, CellStyle> styles;

    /**
     * 需要导出的数据
     */
    private List<T> exportList;

    /**
     * 对象的class对象
     */
    private Class<T> clazz;

    /**
     * 被选中需要导出的字段名称
     */
    private List<String> checkedFieldsName;

    /**
     * 被选中需要导出的字段对象
     */
    private List<Field> checkedFields;

    /**
     * 包含需要字典转换的字段对象
     */
    private List<Field> fieldsContainDict;

    /**
     * 对象中的字典值
     */
    private Map<String, Map<String, String>> dicts;

    private ExcelUtil() {
    }

    public ExcelUtil(Class<T> clazz) {
        this.clazz = clazz;
    }

    /**
     * 将数据导出Excel供浏览器下载
     *
     * @param list 需要导出的数据
     */
    public void exportExcel(List<T> list, HttpServletResponse response) {
        exportExcel(list, null, "sheet", response);
    }

    /**
     * 将数据导出Excel供浏览器下载
     *
     * @param list      需要导出的数据
     * @param sheetName 工作表名称
     */
    public void exportExcel(List<T> list, String sheetName, HttpServletResponse response) {
        exportExcel(list, null, sheetName, response);
    }

    /**
     * 将数据导出Excel自动下载到本地
     *
     * @param list 需要导出的数据
     */
    public void exportExcel(List<T> list) {
        exportExcel(list, null, "sheet");
    }

    /**
     * 将数据导出Excel自动下载到本地
     *
     * @param list      需要导出的数据
     * @param sheetName 工作表名称
     */
    public void exportExcel(List<T> list, String sheetName) {
        exportExcel(list, null, sheetName);
    }

    /**
     * Excel导出数据供浏览器下载
     *
     * @param list       导出的集合数据
     * @param fieldsName 需要导出的字段集合
     * @param sheetName  sheet页/文件名称
     */
    public void exportExcel(List<T> list, List<String> fieldsName, String sheetName, HttpServletResponse response) {
        // 初始化数据
        init(list, sheetName, fieldsName);

        // 转换字典值
        convertDict();

        // sheet第一行加入名称数据
        createTopRow();

        // sheet其他行，添加目标数据
        createOtherRow();

        // 导出wb-浏览器下载excel
        buildExcelDocument(generateFileName(sheetName), wb, response);
    }

    /**
     * 将数据导出Excel自动下载到本地
     *
     * @param list       导出的集合数据
     * @param fieldsName 需要导出的字段集合
     * @param sheetName  sheet页/文件名称
     */
    public void exportExcel(List<T> list, List<String> fieldsName, String sheetName) {
        // 初始化数据
        init(list, sheetName, fieldsName);

        // 转换字典值
        convertDict();

        // sheet第一行加入名称数据作为标题
        createTopRow();

        // sheet其他行，添加目标数据
        createOtherRow();

        // 导出wb
        buildExcelDocument(generateFileName(sheetName), wb);
    }

    /**
     * 浏览器下载excel
     *
     * @param fileName 文件名称
     * @param wb       wb
     * @param response HttpServletResponse
     */
    public void buildExcelDocument(String fileName, Workbook wb, HttpServletResponse response) {
        try {
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader("content-Type", "application/vnd.ms-excel");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
            response.flushBuffer();
            wb.write(response.getOutputStream());
            wb.close();
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 下载Excel到当前目录下
     *
     * @param filename 文件名称
     * @param wb       wb
     */
    public void buildExcelDocument(String filename, Workbook wb) {
        try (OutputStream outFile = new FileOutputStream(filename)) {
            wb.write(outFile);
            wb.close();
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 生成随机名称，防止文件复写
     */
    private String generateFileName(String sheetName) {
        return sheetName + dateTime2String(transform(LocalDateTime.now(), ZoneId.of("+8")),NONE_SEPARATOR) + ".xlsx";
    }

    /**
     * 添加导出数据
     */
    private void createOtherRow() {
        try {
            for (int rowNum = 1; rowNum <= exportList.size(); rowNum++) {
                Row row = sheet.createRow(rowNum);
                T t = exportList.get(rowNum - 1);
                for (int colNum = 0; colNum < checkedFields.size(); colNum++) {
                    Cell cell = row.createCell(colNum);
                    Field field = checkedFields.get(colNum);
                    field.setAccessible(true);

                    cell.setCellStyle(styles.get("data"));

                    // 单元格设置值
                    addCell(cell, field, t);
                }
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 单元格中添加数据
     *
     * @param cell  单元格
     * @param field 字段
     * @param t     list中的一条数据
     */
    private void addCell(Cell cell, Field field, T t) throws IllegalAccessException {
        Class<?> fieldType = field.getType();
        if (String.class == fieldType) {
            cell.setCellValue((String) field.get(t));
        } else if ((Integer.TYPE == fieldType) || (Integer.class == fieldType)) {
            cell.setCellValue((Integer) field.get(t));
        } else if ((Long.TYPE == fieldType) || (Long.class == fieldType)) {
            cell.setCellValue((Long) field.get(t));
        } else if ((Double.TYPE == fieldType) || (Double.class == fieldType)) {
            cell.setCellValue((Double) field.get(t));
        } else if ((Float.TYPE == fieldType) || (Float.class == fieldType)) {
            cell.setCellValue((Float) field.get(t));
        } else if ((BigDecimal.class == fieldType)) {
            if (null != field.get(t)) {
                if (field.get(t).toString().equals("0.00000000") || field.get(t).toString().equals("0E-8")) {
                    cell.setCellValue("0");
                } else {
                    cell.setCellValue(getRateStr(field.get(t).toString()));
                }
            }
        } else if (LocalDateTime.class == fieldType) {
            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
            cell.setCellValue(dateFormat((LocalDateTime) field.get(t), dateFormat));
        } else if (Date.class == fieldType) {
            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
            cell.setCellValue(dateFormat((Date) field.get(t), dateFormat));
        }
    }

    /**
     * 截取小数点后两位
     */
    public String getRateStr(String rateStr) {
        if (rateStr.contains(".")) {
            //获取小数点的位置
            int num = rateStr.indexOf(".");
            //获取小数点后面的数字 是否有两位 不足两位补足两位
            String dotBefore = rateStr.substring(0, num + 1);
            String afterData = rateStr.replace(dotBefore, "");
            if (afterData.length() < 2) {
                afterData = afterData + "0";
            }
            return rateStr.substring(0, num) + "." + afterData.substring(0, 2);
        } else {
            return rateStr;
        }
    }

    /**
     * 时间格式转换
     */
    private String dateFormat(LocalDateTime date, String dateFormat) {
        if (dateFormat == null || "".equals(dateFormat)) {
            dateFormat = "yyyy-MM-dd HH:mm:ss";
        }
        if (null == date) {
            return "";
        }
        DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat);
        return df.format(date);
    }

    /**
     * 时间格式转换
     */
    private String dateFormat(Date date, String dateFormat) {
        if (dateFormat == null || "".equals(dateFormat)) {
            dateFormat = "yyyy-MM-dd HH:mm:ss";
        }

        SimpleDateFormat df = new SimpleDateFormat(dateFormat);
        return df.format(date);
    }

    /**
     * sheet第一行加入名称数据
     */
    private void createTopRow() {
        Row row = sheet.createRow(0);

        sheet.trackAllColumnsForAutoSizing();   // 设置允许自动调整列宽
        for (int index = 0; index < checkedFields.size(); index++) {
            Cell cell = row.createCell(index);
            cell.setCellStyle(styles.get("header"));
            cell.setCellValue(checkedFields.get(index).getAnnotation(Excel.class).name());

            sheet.autoSizeColumn(index);
            //手动调整列宽，解决中文不能自适应问题
            //单元格单行最长支持255*256宽度（每个单元格样式已经设置自动换行，超出即换行）
            //设置最低列宽度，列宽约六个中文字符
            // 第二个参数比例是1:0.303cm，因此11就是列宽3.33cm
            // 第一个参数比例是1:1.482cm，因此
            // A4纸210mm×297mm，横向打印的话，21cm-3.33*2cm=14.34cm，14.34/1.482=9.676
            // A4纸210mm×297mm，纵向打印的话，29.7cm-3.33*2cm=23.04cm，23.04/1.482=15.547
            int width = Math.max(12 * 256, Math.min(255 * 256, sheet.getColumnWidth(index) * 12 / 10));
            sheet.setColumnWidth(index, width);

            if ("headline".equals(checkedFields.get(index).getName())) {
                sheet.setColumnWidth(index, width * 7);
            }
        }
    }

    /**
     * 转换字典值，将数据中字典值转化为对应的值(注:字典值应为String格式)
     */
    private void convertDict() {
        try {
            for (Field field : fieldsContainDict) {
                Excel annotation = field.getAnnotation(Excel.class);
                String dictKey = annotation.dictKey();
                field.setAccessible(true);
                for (T t : exportList) {
                    // 获取字段值
                    Object obj = field.get(t);
                    field.set(t, dicts.get(dictKey).get(obj));
                }
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 初始化
     */
    public void init(List<T> list, String sheetName, List<String> fieldsName) {
        this.checkedFieldsName = fieldsName;

        this.exportList = list;

        // 初始化导出数据
        initExportList();

        // 初始化工作薄
        initWorkbook();

        // 初始化工作表
        initSheet(sheetName);

        // 初始化样式
        initCellStyle();

        // 初始化checkedFields, fieldsContainDict
        initFields();

        // 根据注解生成生成字典
        generateObjDict();
    }

    /**
     * 初始化导出数据
     */
    private void initExportList() {
        if (this.exportList == null) {
            this.exportList = new ArrayList<>();
        }
    }

    /**
     * 初始化工作簿
     */
    private void initWorkbook() {
        this.wb = new SXSSFWorkbook();
    }

    /**
     * 初始化样式
     */
    private void initCellStyle() {
        if (this.styles == null) {
            this.styles = new HashMap<>();
        }

        // 数据格式
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.LEFT);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderRight(BorderStyle.THIN);
        style.setRightBorderColor(IndexedColors.BLACK.getIndex());
        style.setBorderLeft(BorderStyle.THIN);
        style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
        style.setBorderTop(BorderStyle.THIN);
        style.setTopBorderColor(IndexedColors.BLACK.getIndex());
        style.setBorderBottom(BorderStyle.THIN);
        style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
        Font dataFont = wb.createFont();
        dataFont.setFontName("仿宋");
        dataFont.setFontHeightInPoints((short) 16);
        style.setFont(dataFont);
        style.setWrapText(true);    // 自动换行
        styles.put("data", style);

        // 表头格式
        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        Font headerFont = wb.createFont();
        headerFont.setFontName("仿宋");
        headerFont.setFontHeightInPoints((short) 16);
        headerFont.setBold(true);
        headerFont.setColor(IndexedColors.WHITE.getIndex());
        style.setFont(headerFont);
        styles.put("header", style);
    }

    /**
     * 初始化工作表
     */
    private void initSheet(String sheetName) {
        this.sheet = wb.createSheet(sheetName);
        this.sheet.getPrintSetup().setLandscape(true);
    }

    /**
     * 初始化checkedFields, fieldsContainDict
     * fieldsContainDict含有字典表达式的字段
     * checkedFields用户选中的字段
     * 1.如果checkedFieldsName没有定义(未自定义导出字段), 则将所有字段全部导出
     * 2.如果checkedFieldsName进行定义(自定义导出字段), 则根据定义字段进行导出
     */
    private void initFields() {
        // 获取对象所有字段对象
        Field[] fields = clazz.getDeclaredFields();

        // 过滤出checkedFields
        this.checkedFields = Arrays.stream(fields)
                .filter(field -> {
                    if (!CollectionUtils.isEmpty(this.checkedFieldsName)) {
                        if (field.isAnnotationPresent(Excel.class)) {
                            return checkedFieldsName.contains(field.getName());
                        }
                    } else {
                        return field.isAnnotationPresent(Excel.class);
                    }
                    return false;
                })
                .collect(Collectors.toList());

        if (!CollectionUtils.isEmpty(this.checkedFieldsName)) {
            this.checkedFields.sort(Comparator.comparing(field -> checkedFieldsName.indexOf(field.getName())));
        }

        // 过滤出fieldsContainDict
        this.fieldsContainDict = Arrays.stream(clazz.getDeclaredFields())
                .filter(item -> !"".equals(item.getAnnotation(Excel.class) != null ? item.getAnnotation(Excel.class).dictExp() : ""))
                .collect(Collectors.toList());
    }

    /**
     * 通过扫描字段注解生成字典数据
     */
    private void generateObjDict() {
        if (CollectionUtils.isEmpty(fieldsContainDict)) {
            return;
        }

        if (dicts == null) {
            dicts = new HashMap<>();
        }

        for (Field field : fieldsContainDict) {
            String dictKey = field.getAnnotation(Excel.class).dictKey();
            String exps = field.getAnnotation(Excel.class).dictExp();
            String[] expArr = exps.split(",");

            Map<String, String> entry = new HashMap<>();

            for (String exp : expArr) {
                String[] pair = exp.split("=");
                entry.put(pair[0], pair[1]);
            }

            dicts.put(dictKey, entry);
        }
    }

    /**
     * 解析Excel文件
     *
     * @param file Excel文件
     * @return 列数可变的二维集合，将Excel表格中从第二行开始的每行存放在行中，每列存放在列中
     */
    public static List<List<String>> parse(MultipartFile file) {
        if (file == null) {
            throw new RuntimeException();
        }

        String filename = file.getOriginalFilename();
        if (StringUtils.isEmpty(filename)) {
            throw new RuntimeException();
        }

        Workbook workbook;
        try {
            InputStream in = file.getInputStream();
            if (filename.endsWith(".xls")) {
                workbook = new HSSFWorkbook(in);
            } else if (filename.endsWith(".xlsx")) {
                workbook = new XSSFWorkbook(in);
            } else {
                throw new RuntimeException(filename);
            }

            List<List<String>> result = new ArrayList<>();

            Sheet sheet = workbook.getSheetAt(0);   // 获取第一张工作表
            int minRowCount = sheet.getFirstRowNum();     // 第一行
            int maxRowCount = sheet.getLastRowNum();      // 最后一行
            for (int i = minRowCount + 1; i <= maxRowCount; i++) {    // 从第二行开始读取，因为第一行是标题
                Row row = sheet.getRow(i);
                int minCellCount = row.getFirstCellNum(); // 第一列
                int maxCellCount = row.getLastCellNum();  // 最后一列+1，最后一列只截至到非空的单元格
                List<String> cells = new ArrayList<>();
                for (int j = minCellCount; j < maxCellCount; j++) {
                    Cell cell = row.getCell(j, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); // 当取到空字符串时，返回null
                    if (cell != null) {
                        cells.add(cell.getStringCellValue().trim());
                    } else {
                        cells.add("");
                    }
                }
                result.add(cells);
            }

            workbook.close();

            return result;
        } catch (IOException e) {
            throw new RuntimeException(filename, e);
        }
    }
}